﻿namespace NTLib.IOC
{
    using Containers;
    using Core.Collection;
    using Core.Log;
    using Core.Reflection;
    using Core.Tasks;
    using Core.Threading;
    using Exceptions;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;

    /// <summary>
    /// IOContainer used to provide instance throught an application following configuration.
    /// Permit to isolate the definition and the implementation.
    /// </summary>
    [ThreadSafe]
    public class IOContainer : CancellableObject
    {
        #region Fields

        private readonly ReaderWriterLockSlim _lockerHierarchi;
        private readonly Dictionary<Guid, IOContainer> _childrens;

        private IOContainer _parentContainer;

        private readonly Guid _id = Guid.NewGuid();
        private readonly Dictionary<Type, IContainer> _containers;
        private readonly ReaderWriterLockSlim _locker;
        private readonly string _name;

        private bool _hierarchicalSearch = true;
        private bool _allowInheritanceKeySearch = true;

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes a new instance of the <see cref="IOContainer"/> class.
        /// </summary>
        public IOContainer(string name = null)
        {
            this._name = name;
            this._containers = new Dictionary<Type, IContainer>();
            this._locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
            this._lockerHierarchi = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

            this._childrens = new Dictionary<Guid, IOContainer>();

            if (string.IsNullOrEmpty(name))
                this._name = Guid.NewGuid().ToString();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the identifier.
        /// </summary>
        public Guid Id
        {
            get { return this._id; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Get method could search into all the IOContainer registred into the global collection.
        /// </summary>
        /// <value>
        ///   <c>true</c> if enabled global search; otherwise, <c>false</c>.
        /// </value>
        public bool EnabledHierarchiSearch
        {
            get
            {
                using (_lockerHierarchi.LockRead(base.CancelToken))
                    return this._hierarchicalSearch;
            }
            set
            {
                using (_lockerHierarchi.LockWrite(base.CancelToken))
                    this._hierarchicalSearch = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Get method could use the Key also by inheritance relation.
        /// </summary>
        /// <value>
        /// <c>true</c> if [allow inheritance search]; otherwise, <c>false</c>.
        /// </value>
        public bool AllowInheritanceKeySearch
        {
            get
            {
                using (this._locker.LockRead(base.CancelToken))
                    return this._allowInheritanceKeySearch;
            }
            set
            {
                using (this._locker.LockWrite(base.CancelToken))
                    this._allowInheritanceKeySearch = value;
            }
        }

        /// <summary>
        /// Gets the name.
        /// </summary>
        public string Name
        {
            get { return this._name; }
        }

        /// <summary>
        /// Gets the parent.
        /// </summary>
        public IOContainer Parent
        {
            get
            {
                using (this._lockerHierarchi.LockRead(base.CancelToken))
                {
                    return this._parentContainer;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets an instance associate to the type pass in argument.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance expected and associate.</typeparam>
        /// <returns>Intance type <typeparamref name="TInstance"/>; otherwise default(<typeparamref name="TInstance"/>)</returns>
        public TInstance Get<TInstance>(Predicate<TInstance> predicate = null)
        {
            return Get<TInstance, TInstance>(predicate);
        }

        /// <summary>
        /// Gets an instance associate to the type pass in argument.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance expected.</typeparam>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <returns>
        /// Intance type <typeparamref name="TInstance" />; otherwise default(<typeparamref name="TInstance" />)
        /// </returns>
        /// <exception cref="System.AggregateException"></exception>
        public TInstance Get<TInstance, TKey>(Predicate<TInstance> predicate = null)
        {
            return this.Get<TInstance>(typeof(TKey), predicate);
        }

        /// <summary>
        /// Gets the specified key.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <param name="key">The key.</param>
        /// <returns>
        /// Intance type <typeparamref name="TInstance" />; otherwise default(<typeparamref name="TInstance" />)
        /// </returns>
        /// <exception cref="System.AggregateException"></exception>
        public TInstance Get<TInstance>(Type key, Predicate<TInstance> predicate = null)
        {
            List<Exception> exs = null;
            TInstance inst = default(TInstance);
            using (_lockerHierarchi.LockRead(base.CancelToken))
            {
                try
                {
                    inst = this.GetImpl<TInstance>(key);

                    if (predicate != null &&
                        !EqualityComparer<TInstance>.Default.Equals(inst, default(TInstance)) &&
                        !predicate(inst))
                        inst = default(TInstance);

                    if (this._hierarchicalSearch && this._parentContainer != null &&
                        EqualityComparer<TInstance>.Default.Equals(inst, default(TInstance)))
                        inst = this._parentContainer.GetImpl<TInstance>(key);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    if (exs == null)
                    {
                        if (exs == null)
                            exs = new List<Exception>();
                        exs.Add(ex);
                    }
                }
            }

            if (exs != null && exs.Any())
                throw new AggregateException(exs);
            return inst;
        }

        /// <summary>
        /// Store the specific instance using the <typeparamref name="TKey"/> as associate key.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <param name="inst">The instance.</param>
        public void Store<TInstance, TKey>(TInstance inst, bool @override = false)
        {
            Store<TKey>(new InstanceContainer(inst, typeof(TKey)), @override);
        }
        /// <summary>
        /// Store the specific instance using the <typeparamref name="TInstance"/> as associate keys
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <param name="inst">The inst.</param>
        public void Store<TInstance>(TInstance inst, bool @override = false)
        {
            Store<TInstance>(new InstanceContainer(inst, typeof(TInstance)), @override);
        }

        /// <summary>
        /// Store the specified provider function for the specific <typeparamref name="TInstance"/> key.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <param name="provider">The provider.</param>
        public void Store<TInstance>(Func<object, CancellationToken, TInstance> provider, bool @override = false)
        {
            if (provider == null)
                throw new ArgumentNullException(nameof(provider));

            Store<TInstance>(new ProviderContainer<TInstance>(provider), @override);
        }

        /// <summary>
        /// Adds the specified use pool.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <param name="usePool">if set to <c>true</c> [use pool].</param>
        public void Store<TInstance, TKey>(bool usePool, bool @override = false)
        {
            Store<TKey>(new TypeContainer(typeof(TInstance), typeof(TKey), usePool), @override);
        }

        /// <summary>
        /// Adds the specified container.
        /// </summary>
        /// <typeparam name="TKey">The type of the Key.</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="override">if set to <c>true</c> will override if the container already exist.</param>
        public void Store<TKey>(IContainer container, bool @override = false)
        {
            using (this._locker.LockWrite())
            {
                Type keyType = typeof(TKey);

                if (@override && this._containers.ContainsKey(keyType))
                    this._containers.Remove(keyType);
                this._containers.Add(keyType, container);
            }
        }

        /// <summary>
        /// Forgets the containers associate to the key <typeparamref name="TKey"/> 
        /// </summary>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        public void Forget<TKey>()
        {
            using (this._locker.LockWrite())
            {
                Type keyType = typeof(TKey);
                IContainer container = null;
                if (this._containers.TryGetValue(keyType, out container))
                {
                    this._containers.Remove(keyType);
                    try
                    {
                        container.Dispose();
                    }
                    catch (Exception ex)
                    {
                        NTLog.Instance.RecordError(ex, nameof(IOContainer));
                    }
                }
            }
        }

        /// <summary>
        /// Attach the ioContainer as child.
        /// </summary>
        /// <param name="ioContainer">The io container.</param>
        /// <exception cref="ArgumentNullException">Raised if the argument pass is null</exception>
        /// <exception cref="InvalidOperationException">Raised if the current argument is itself.</exception>
        /// <exception cref="NTLib.IOC.Exceptions.IOContainerAlreadyAttachedException">if the container pass in argument already have a parent</exception>
        /// <remarks>
        /// If the current container came to be disposed automatically the children will be attached to the parent of the current instance.
        /// </remarks>
        public void Attach(IOContainer ioContainer)
        {
            if (ioContainer == null)
                throw new ArgumentNullException(nameof(ioContainer));

            if (ioContainer == this)
                throw new InvalidOperationException();

            using (_lockerHierarchi.LockWrite())
            {
                if (this._childrens.ContainsKey(ioContainer.Id))
                    return;
                if (ioContainer._parentContainer != null || ioContainer == this._parentContainer)
                    throw new IOContainerAlreadyAttachedException(this._name);
                ioContainer._parentContainer = this;
                _childrens.Add(ioContainer.Id, ioContainer);
            }
        }

        /// <summary>
        /// Removes the container.
        /// </summary>
        /// <param name="containerID">The container identifier.</param>
        public void Dettach(Guid id)
        {
            using (_lockerHierarchi.LockWrite())
            {
                IOContainer children = null;
                if (this._childrens.TryGetValue(id, out children))
                {
                    children._parentContainer = null;
                    this._childrens.Remove(id);
                }
            }
        }

        /// <summary>
        /// Gets the implementation.
        /// </summary>
        /// <typeparam name="TInstance">The type of the instance.</typeparam>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <returns></returns>
        private TInstance GetImpl<TInstance>(Type key)
        {
            using (this._locker.LockRead(base.CancelToken))
            {
                IContainer container = null;
                if (!this._containers.TryGetValue(key, out container) && this._allowInheritanceKeySearch)
                {
                    container = (from c in this._containers
                                 where c.Key.IsOrInheritOf(key) && c.Value.CanHandle(key)
                                 select c.Value).FirstOrDefault();
                }

                if (container == null || !container.CanHandle(key))
                    return default(TInstance);

                return container.Get<TInstance>(key);
            }
        }

        /// <summary>
        /// Dispose methods called whenever is the dipose origin.
        /// </summary>
        protected override void DisposeEveryTime()
        {
            using (this._lockerHierarchi.LockWrite(base.CancelToken))
            {
                foreach (var child in this._childrens)
                {
                    try
                    {
                        if (this._parentContainer != null)
                        {
                            child.Value._parentContainer = null;
                            this._parentContainer.Attach(child.Value);
                        }
                    }
                    catch (Exception ex)
                    {
                        NTLog.Instance.RecordError(ex, nameof(IOContainer), this._name);
                    }
                }
                this._childrens.Clear();
            }

            using (this._locker.LockWrite(base.CancelToken))
            {
                foreach (var container in this._containers)
                {
                    try
                    {
                        container.Value.Dispose();
                    }
                    catch (Exception ex)
                    {
                        NTLog.Instance.RecordError(ex, nameof(IOContainer), this._name);
                    }
                }
            }

            this._locker.Dispose();
            this._lockerHierarchi.Dispose();

            base.DisposeEveryTime();
        }

        #endregion
    }
}
